﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace thailandProject.Models
{
    /// <summary>
    /// Contains a function to get the currently available properties and any partial matches
    /// </summary>
    public class PropertyMatch
    {
        public DateTime startDate;
        public DateTime endDate;
        public PROPERTY property;
        public int startDaysFromNow;
        public int endDaysFromNow;
        public decimal quotedPrice;
        public bool isPartial;
        public bool isOnHold;

        /// <summary>
        /// Used with the partial dictionary. It houses a reservation date pair when comparing the
        /// reservation's start and end dates
        /// </summary>
        private class DatePair
        {
            public DateTime startDate;
            public DateTime endDate;
            public int propID;
        }

        /// <summary>
        /// Returns all the complete and partial matches within the start and end dates
        /// </summary>
        /// <param name="start">The start date of the range test</param>
        /// <param name="end">The end date of the range test</param>
        /// <returns>A list containing complete and partial matches</returns>
        public static List<List<PropertyMatch>> getPropertyMatches(DateTime start, DateTime end)
        {
            List<List<PropertyMatch>> rt = new List<List<PropertyMatch>>();

            try
            {
                List<PropertyMatch> complete = new List<PropertyMatch>();
                List<PropertyMatch> partial = new List<PropertyMatch>();

                //Get the open reservations. Do this by comparing it to every reservation
                using (ThailandEntities a = new ThailandEntities())
                {
                    //Select all properties that do not have a reservation in it
                    var comMatch = from properties in a.PROPERTies
                                   where !(from filledRooms in a.PROPERTies
                                           from reservations in a.RESERVATIONs
                                           where (filledRooms.propID == reservations.propID) &&
                                               (start < reservations.resEndDate &&
                                               end > reservations.resStartDate)
                                           select filledRooms.propID).Contains(properties.propID) &&
                                           properties.propIsActive == true
                                   orderby properties.propName
                                   select properties;

                    //Select all properties in that have a reservation which overlap the current date range, but is OnHold
                    //Because a property is OnHold, it doesn't have a set date range. Therefore, an OnHold match must have
                    //dates which equal is larger than the queried date range.
                    var comOnHoldMatch = from reservations in a.RESERVATIONs
                                         where ((reservations.resEndDate > start && reservations.resEndDate <= end) ||
                                               (reservations.resStartDate < end && reservations.resStartDate >= start) ||
                                               (reservations.resStartDate > start && reservations.resEndDate < end)) &&
                                               reservations.PROPERTY.propIsActive == true
                                         orderby reservations.resStartDate
                                         select reservations;

                    List<PROPERTY> comp = comMatch.ToList();
                    List<RESERVATION> compOnHold = comOnHoldMatch.ToList();
                    foreach (PROPERTY p in comp)
                    {
                        PropertyMatch pm = new PropertyMatch();
                        pm.startDate = start;
                        pm.endDate = end;
                        pm.property = p;
                        pm.isOnHold = false;
                        complete.Add(pm);
                    }
                    //Add the onHold properties. This shouldn't cause repeats because it's selecting only ones that the previous LINQ ignored
                    Dictionary<int, PROPERTY> badProps = new Dictionary<int, PROPERTY>();
                    foreach (RESERVATION r in compOnHold)
                    {
                        //Search through each reservation. If it is not held, it's an unavailable property
                        if (!r.resIsHeld)
                        {
                            if (badProps.ContainsKey(r.propID))
                            {
                                badProps.Remove(r.propID);
                                badProps.Add(r.propID, null);
                            }
                            else
                                badProps.Add(r.propID, null);

                        }
                        else
                        {
                            if (badProps.ContainsKey(r.propID))
                            {
                                //Do nothing
                            }
                            else
                                badProps.Add(r.propID, r.PROPERTY);
                        }
                    }

                    foreach (KeyValuePair<int, PROPERTY> kvp in badProps)
                    {
                        if (kvp.Value != null)
                        {
                            if (comp.Contains(kvp.Value))
                                continue;

                            PropertyMatch pm = new PropertyMatch();
                            pm.startDate = start;
                            pm.endDate = end;
                            pm.property = kvp.Value;
                            pm.isOnHold = true;
                            complete.Add(pm);
                        }
                    }
                    //Select partial matches. Partial matches select apartments within the
                    //daterange, but only ones where it's reservation overlaps slightly
                    //into the testing range

                    /*var partResNew = from properties in a.PROPERTies
                                     where (from filledRooms in a.RESERVATIONs
                                            where (filledRooms.resEndDate > start &&
                                               filledRooms.resEndDate <= end) ||
                                               (filledRooms.resStartDate < end &&
                                               filledRooms.resStartDate >= start)
                                            select filledRooms.propID).Contains(properties.propID)
                                     orderby properties.propName
                                     select properties;*/

                    //Include reservations where the end or start date overlaps with the start or end date, respectively.
                    //This will generate a list with a lot of duplicates (i.e. reservations overlapping). This is okay
                    //becuse the next chunk of code will put them inside a dictionary and determine the available date
                    //ranges
                    var partRes = from reservations in a.RESERVATIONs
                                  where ((reservations.resEndDate > start && reservations.resEndDate <= end) ||
                                        (reservations.resStartDate < end && reservations.resStartDate >= start)) &&
                                        reservations.PROPERTY.propIsActive == true &&
                                        reservations.resIsHeld == false
                                  orderby reservations.resStartDate
                                  select reservations;

                    //For OnHold partial reservations, the same selection alorgithm applies.
                    var partOnHoldRes = from reservations in a.RESERVATIONs
                                        where ((reservations.resEndDate > start && reservations.resEndDate <= end) ||
                                              (reservations.resStartDate < end && reservations.resStartDate >= start)) &&
                                              reservations.PROPERTY.propIsActive == true &&
                                              reservations.resIsHeld == true
                                        orderby reservations.resStartDate
                                        select reservations;

                    List<RESERVATION> res = partRes.ToList();
                    Dictionary<int, List<DatePair>> partDictionary = new Dictionary<int, List<DatePair>>();

                    List<RESERVATION> onHoldRes = partOnHoldRes.ToList(); //This will be evaluated later...
                    Dictionary<int, List<DatePair>> partOnHoldDictionary = new Dictionary<int, List<DatePair>>();

                    //Load the values into the dictionary
                    foreach (RESERVATION r in res)
                    {
                        //Before adding the property to the dictionary, see if the property already exists
                        List<DatePair> values;
                        if (partDictionary.TryGetValue(r.propID, out values))
                        {
                            //If so, add the new value to the list's dictionary
                            DatePair newPair = new DatePair();
                            newPair.startDate = r.resStartDate;
                            newPair.endDate = r.resEndDate;
                            values.Add(newPair);
                        }
                        else
                        {
                            //Add this reservation's property details to the dictionary
                            List<DatePair> newList = new List<DatePair>();
                            DatePair newPair = new DatePair();
                            newPair.startDate = r.resStartDate;
                            newPair.endDate = r.resEndDate;
                            newList.Add(newPair);

                            partDictionary.Add(r.propID, newList);
                        }
                    }

                    //Now that all reservations are loaded on a property basis
                    Dictionary<int, List<DatePair>> compressedList = new Dictionary<int, List<DatePair>>();
                    foreach (KeyValuePair<int, List<DatePair>> kvp in partDictionary)
                    {
                        //Each kvp represents a room. Let's construct a replacement list that combines overlapping reservations
                        DatePair[] dps = kvp.Value.ToArray(); //Array for easy indexing
                        List<DatePair> replacement = new List<DatePair>(); //Replacement list for combined reservation times
                        replacement.Add(dps[0]); //Add initial record for editing

                        for (int index = 0; index < dps.Length - 1; index++)
                        {
                            //If the next entry begins before or on the ending day, combine the two
                            //It shouldn't ever begin before... but just in case! Also, old data (not booked under the system) may be double-booked
                            if (dps[index].endDate >= dps[index + 1].startDate)
                            {
                                DatePair dp = replacement.Last(); //Get the last element
                                dp.endDate = dps[index + 1].endDate; //Set the end date to be the next item's end date
                            }
                            else
                            {
                                //There is potentially a break between these properties, so add a new DatePair
                                replacement.Add(dps[index + 1]); //The last value will be used at the start of the next one
                            }
                        }//End to for index
                        compressedList.Add(kvp.Key, replacement);

                    }//End to foreach kvp

                    //Now that the records are compressed, we need to "invert" them and generate a list of open times
                    Dictionary<int, List<DatePair>> openTimes = new Dictionary<int, List<DatePair>>();
                    foreach (KeyValuePair<int, List<DatePair>> kvp in compressedList)
                    {
                        List<DatePair> openTimesProp = new List<DatePair>();

                        //The first and last pairs are special cases, so handle those outside the list
                        DatePair first = new DatePair();
                        if (kvp.Value.First().startDate > start)
                        {
                            //If there's a gap between the start and first value's start date, use that
                            first.startDate = start;
                            first.endDate = kvp.Value.First().startDate;
                        }
                        else
                        {
                            //Use the first value's end date as the start
                            first.startDate = kvp.Value.First().endDate;

                            if (kvp.Value.Count == 1) //If there is only one record
                                first.endDate = end; //The end date is the one the user specified
                            else
                                first.endDate = kvp.Value[1].startDate; //Get the second element's start date
                        }
                        openTimesProp.Add(first);

                        for (int i = 1; i < kvp.Value.Count - 1; i++)
                        {
                            DatePair openTime = new DatePair();

                            openTime.startDate = kvp.Value[i].endDate;
                            openTime.endDate = kvp.Value[i + 1].startDate;

                            openTimesProp.Add(openTime);
                        }

                        //Check for a closing case
                        if (kvp.Value.Last().endDate < end)
                        {
                            //If the last end date is before the user's specified end date
                            DatePair last = new DatePair();
                            last.startDate = kvp.Value.Last().endDate;
                            last.endDate = end;
                            openTimesProp.Add(last);
                        }
                        else if(kvp.Value.Count > 1)
                        {
                            //Use the last()'s startdate as the end date
                            //User the last - 1's enddate as the start date
                            DatePair last = new DatePair();
                            last.startDate = kvp.Value[kvp.Value.Count - 2].endDate;
                            last.endDate = kvp.Value.Last().startDate;
                            openTimesProp.Add(last);
                        }

                        openTimes.Add(kvp.Key, openTimesProp);
                    }//End to foreach kvp

                    //Filter out all duplicate entries and generate a properties list
                    List<PROPERTY> part = res.GroupBy(r => r.propID).Select(rr => rr.First()).Select(rrr => rrr.PROPERTY).ToList();
                    //List<PROPERTY> part = partResNew.ToList().GroupBy(p => p.propID).Select(pp => pp.First()).ToList();

                    if (openTimes.Count > 0)
                    {
                        int currentPropID = -1;
                        foreach (PROPERTY p in part)
                        {
                            currentPropID = p.propID;
                            List<DatePair> ldp;
                            if (openTimes.TryGetValue(p.propID, out ldp))
                            {
                                foreach (DatePair dp in ldp)
                                {
                                    //if the timespan is less than two days long, ignore it
                                    if ((dp.endDate - dp.startDate).Days < 2)
                                        continue;
                                    //If this record is a duplicate, skip it
                                    if (partial.Count > 0)
                                        if (partial.Last().startDate == dp.startDate && partial.Last().endDate == dp.endDate && partial.Last().property.propID == currentPropID)
                                            continue;

                                    PropertyMatch pm = new PropertyMatch();
                                    pm.startDate = dp.startDate;
                                    pm.endDate = dp.endDate;
                                    pm.property = p;
                                    pm.startDaysFromNow = (pm.startDate.Date - DateTime.Now.Date).Days;
                                    pm.endDaysFromNow = (pm.endDate.Date - DateTime.Now.Date).Days;
                                    pm.isOnHold = false;
                                    partial.Add(pm);
                                }
                            }
                        }//End to for each property in part
                    }//End to if openTimes count > 0

                    //Now that the non-OnHold stuff has been processed, process the OnHold partial matches
                    //This will be handled slightly differently than the complete matches. Rather than
                    //determining the open times from closed reservations, the OnHold reservation's dates
                    //are fair game because they are not confirmed.
                    //
                    //Load the values into the dictionary on a property basis
                    foreach (RESERVATION r in onHoldRes)
                    {
                        //Before adding the property to the dictionary, see if the property already exists
                        List<DatePair> values;
                        if (partOnHoldDictionary.TryGetValue(r.propID, out values))
                        {
                            //If so, add the new value to the list's dictionary
                            DatePair newPair = new DatePair();
                            newPair.startDate = r.resStartDate;
                            newPair.endDate = r.resEndDate;
                            values.Add(newPair);
                        }
                        else
                        {
                            //Add this reservation's property details to the dictionary
                            List<DatePair> newList = new List<DatePair>();
                            DatePair newPair = new DatePair();
                            newPair.startDate = r.resStartDate;
                            newPair.endDate = r.resEndDate;
                            newList.Add(newPair);

                            partOnHoldDictionary.Add(r.propID, newList);
                        }
                    }
                    //Compress the list now
                    Dictionary<int, List<DatePair>> compressedOnHoldList = new Dictionary<int, List<DatePair>>();
                    foreach (KeyValuePair<int, List<DatePair>> kvp in partOnHoldDictionary)
                    {
                        //Each kvp represents a room. Let's construct a replacement list that combines overlapping reservations
                        DatePair[] dps = kvp.Value.ToArray(); //Array for easy indexing
                        List<DatePair> replacement = new List<DatePair>(); //Replacement list for combined reservation times
                        replacement.Add(dps[0]); //Add initial record for editing

                        for (int index = 0; index < dps.Length - 1; index++)
                        {
                            //If the next entry begins before or on the ending day, combine the two
                            //It shouldn't ever begin before... but just in case!
                            if (dps[index].endDate >= dps[index + 1].startDate)
                            {
                                DatePair dp = replacement.Last(); //Get the last element
                                dp.endDate = dps[index + 1].endDate; //Set the end date to be the next item's end date
                            }
                            else
                            {
                                //There is potentially a break between these properties, so add a new DatePair
                                replacement.Add(dps[index + 1]); //The last value will be used at the start of the next one
                            }
                        }//End to for index
                        compressedOnHoldList.Add(kvp.Key, replacement);

                    }//End to foreach kvp
                    //Now that they have been compressed, ensure that the start and end times do not exceed the query dates
                    foreach (KeyValuePair<int, List<DatePair>> kvp in compressedOnHoldList)
                    {
                        foreach (DatePair dp in kvp.Value)
                        {
                            if (dp.startDate < start)
                                dp.startDate = start;
                            if (dp.endDate > end)
                                dp.endDate = end;
                        }//End to for each dp
                    }//End to for each kvp
                    //Add them to the partial list now
                    List<PROPERTY> partOnHold = onHoldRes.GroupBy(r => r.propID).Select(rr => rr.First()).Select(rrr => rrr.PROPERTY).ToList();
                    //Get a list on basis of property
                    if (compressedOnHoldList.Count > 0)
                    {
                        foreach (PROPERTY p in partOnHold)
                        {
                            List<DatePair> ldp;
                            if (compressedOnHoldList.TryGetValue(p.propID, out ldp))
                            {
                                foreach (DatePair dp in ldp)
                                {
                                    //if the timespan is less than two days long, ignore it
                                    if ((dp.endDate - dp.startDate).Days < 2)
                                        continue;
                                    //If this record is a duplicate, skip it
                                    if (partial.Count > 0)
                                        if (partial.Last().startDate == dp.startDate && partial.Last().endDate == dp.endDate)
                                            continue;

                                    PropertyMatch pm = new PropertyMatch();
                                    pm.startDate = dp.startDate;
                                    pm.endDate = dp.endDate;
                                    pm.property = p;
                                    pm.startDaysFromNow = (pm.startDate.Date - DateTime.Now.Date).Days;
                                    pm.endDaysFromNow = (pm.endDate.Date - DateTime.Now.Date).Days;
                                    pm.isOnHold = true;
                                    partial.Add(pm);
                                }
                            }
                        }//End to for each property in part
                    }


                    #region Old Code
                    //Skip this stuff if no reservations were returned
                    /*if (partDictionary.Count > 0)
                    {
                        /*int[] propId = new int[res.Count];
                        int counter = 0;
                        foreach (RESERVATION r in res)
                        {
                            propId[counter++] = r.propID;
                        }

                        var partMatch = from properties in a.PROPERTies
                                        where propId.Any(p => p == properties.propID)
                                        orderby properties.propID
                                        select properties;
                        List<PROPERTY> part = partMatch.ToList();

                        foreach (PROPERTY p in part)
                        {
                            PropertyMatch pm = new PropertyMatch();
                            DatePair dp;
                            partDictionary.TryGetValue(p.propID, out dp);

                            //Throw out any matches where the DatePair's start and end date is inside the queried range,
                            //or the queried range is inside the DatePair's
                            if (dp.startDate >= start && dp.endDate <= end)
                                continue;
                            if (dp.startDate <= start && dp.endDate >= end)
                                continue;

                            if (dp.endDate < end)
                            {
                                pm.startDate = dp.endDate;
                                pm.endDate = end;
                            }
                            else
                            {
                                pm.startDate = start;
                                pm.endDate = dp.startDate;
                            }
                            pm.property = p;
                            pm.startDaysFromNow = (pm.startDate.Date - DateTime.Now.Date).Days;
                            pm.endDaysFromNow = (pm.endDate.Date - DateTime.Now.Date).Days;

                            if (pm.startDate.Date != pm.endDate.Date)
                            {
                                pm.isOnHold = false;
                                partial.Add(pm);
                            }
                        }
                    }//End to if res.Count > 0*/
                    #endregion
                }//End to using

                //Filter out any partial matches that are a "repeat" of a complete match. Due to On Hold reservations,
                //it is possible to generate an On Hold partial match that is essentially the same as a complete match
                List<PropertyMatch> removeList = new List<PropertyMatch>();
                foreach (PropertyMatch pm in complete)
                {
                    foreach (PropertyMatch part in partial)
                    {

                        if (part.property.propID == pm.property.propID)
                        {
                            if (part.startDate >= pm.startDate &&
                                part.endDate <= pm.endDate)
                                removeList.Add(part);
                        }
                    }
                }
                
                //Remove all occurances found in removeList
                foreach (PropertyMatch pm in removeList)
                    partial.Remove(pm);

                rt.Add(complete);
                rt.Add(partial);
            }
            catch (Exception ex)
            {
                ErrorLog.logError(ex);
            }

            return rt;
        }

        public static List<PropertyMatch> getPropertiesFromIds(int[] propIds)
        {
            List<PropertyMatch> rt = new List<PropertyMatch>();
            try
            {
                using (ThailandEntities a = new ThailandEntities())
                {
                    //Select all properties that match any given id in the propIds array
                    var properties = from prop in a.PROPERTies
                                     where propIds.Any(id => id == prop.propID)
                                     select prop;

                    //Convert each PROPERTY to a PropertyMatch object
                    List<PROPERTY> propertyList = properties.ToList();
                    foreach (PROPERTY p in propertyList)
                    {
                        PropertyMatch pm = new PropertyMatch();
                        pm.property = p;
                        rt.Add(pm);
                    }
                }
            }
            catch (Exception ex)
            {
                ErrorLog.logError(ex);
            }

            return rt;
        }

        /// <summary>
        /// Intended for use when the client has entered values for the selected property(ies)
        /// prices. This parses out the prices and sets the corresponding list's properties
        /// to have the value
        /// </summary>
        /// <param name="list">List containing all used PropertyMatches</param>
        /// <param name="data">FromCollection data containing prices</param>
        public static void setPropertyPrices(List<PropertyMatch> list, FormCollection data)
        {
            try
            {
                foreach (PropertyMatch pm in list)
                {
                    pm.quotedPrice = Convert.ToDecimal(data["quotedPrice" + pm.property.propID]);
                }
            }
            catch (Exception ex)
            {
                ErrorLog.logError(ex);
            }
        }
    }
}